package org.bhf.providers.security;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Token used to guard against Cross Site Request Forgeries. Tokens are good for 24 hours. Pages that are active
 * for more than one day must reload in order to refresh the token.
 */
final class CSRFToken
{
    private String  token;

    /**
     * Construct a token for the given ServiceGroup and Service
     * @param serviceGroupName Must not be <code>null</code>
     * @param serviceName Must not be <code>null</code>
     */
    CSRFToken( final String serviceGroupName, final String serviceName )
    {
        token = serviceGroupName + ":" + serviceName + ":" + Long.toString( System.currentTimeMillis(), Character.MAX_RADIX );
    }

    /**
     * Construct a token from its encrypted form. A <code>EncryptionFacility</code> must have been bound to a Project
     * Injector.
     * @param encryptedToken As returned by <code>encrypt()</code>
     * @param password For PBE decryption
     */
    CSRFToken( final String encryptedToken, final char[] password )
    {
        try
        {
            token = (String)decrypt( encryptedToken, password );
        }
        catch( final IOException ioe ) // Not in memory
        {
            token = "";
        }
        catch( ClassNotFoundException cnfe ) // Can't be missing java.lang.String
        {
            token = "";
        }
    }

    /**
     * Return the encrypted form of this token. A <code>EncryptionFacility</code> must have been bound to a Project
     * Injector.
     * @param password For PBE encryption
     * @return The encrypted form of this token.
     */
    String   encrypt( final char[] password )
    {
        try
        {
            return encrypt( token, password );
        }
        catch( IOException ioe ) // Can't happen in memory
        {
            throw new RuntimeException( ioe );
        }
    }

    /**
     * Verify this token agains given ServiceGroup and Service
     * @param serviceGroupName Must not be <code>null</code>
     * @param serviceName Must not be <code>null</code>
     * @param toleranceMillis Length of time that a token is good
     * @return <code>true</code> if this token is valid.
     */
    public boolean  verify( final String serviceGroupName, final String serviceName, final long toleranceMillis )
    {
        try
        {
            final String[] e = token.split( ":" );

            return
                e.length == 3 &&
                serviceGroupName.equals( e[0] ) &&
                serviceName.equals( e[1] ) &&
                (System.currentTimeMillis() - Long.parseLong( e[2], Character.MAX_RADIX )) < toleranceMillis;
        }
        catch( final Exception e )
        {
            return false;
        }
    }

    //
    // Private
    //

    private String  encrypt( final Object object, final char[] password )
        throws IOException
    {
        final ByteArrayOutputStream bout = new ByteArrayOutputStream();

        if( object instanceof String )
        {
            bout.write( new byte[] { 13 } );
            bout.write( object.toString().getBytes( "UTF-8" ) );
        }
        else
        {
            final ObjectOutputStream out  = new ObjectOutputStream( bout );

            bout.write( new byte[] { 12 } );
            out.writeObject( object );
            out.flush();
            out.close();
        }

        // Some app servers can't handle the trailing ='s in base64 padding
        final byte[]    bytes   = Crypto.encrypt( bout.toByteArray(), password );

        return Base64.encodeBytes( bytes, Base64.URL_SAFE | Base64.DONT_BREAK_LINES ).replace( '=', '*' );
    }

    private Object  decrypt( final String encrypted, final char[] password )
        throws IOException, ClassNotFoundException
    {
        final byte[] bytes = Crypto.decrypt( Base64.decode( encrypted.replace( '*', '=' ), Base64.URL_SAFE ), password );

        if( bytes[0] == 13 )
        {
            return new String( bytes, 1, bytes.length - 1, "UTF-8" );
        }
        else
        {
            final ByteArrayInputStream bin     = new ByteArrayInputStream( bytes, 1, bytes.length - 1 );
            final ObjectInputStream in      = new ObjectInputStream( bin );

            try
            {
                return in.readObject();
            }
            finally
            {
                in.close();
            }
        }
    }
}
